Skip to content

fix(coding-agent): keep host credentials out of the kernel and its state snapshots - #2174

Draft
kevinjosethomas wants to merge 1 commit into
mainfrom
eng-5342-kernel-secret-retention
Draft

fix(coding-agent): keep host credentials out of the kernel and its state snapshots#2174
kevinjosethomas wants to merge 1 commit into
mainfrom
eng-5342-kernel-secret-retention

Conversation

@kevinjosethomas

@kevinjosethomas kevinjosethomas commented Sep 9, 2026

Copy link
Copy Markdown
Member

Context

Linear: ENG-5342 — https://linear.app/primeintellect/issue/ENG-5342

Root cause: ReplKernelManager.doStart spawned the REPL kernel with { ...process.env, ...options.env }, so every host credential (PRIME_API_KEY, OPENAI_API_KEY, ...) was readable from kernel and bash() cells via os.environ. Anything the model assigned from it was pickled by the per-session snapshot into session-artifacts/<id>/kernel-state.dill and revived on resume, so scrubbing the (0644) transcript did not remove it. There was no redaction and no way to turn snapshots off.

Changes

  1. Kernel environment allowlist (src/core/kernel/kernel-env.ts). The kernel receives an allowlisted subset of the host env (PATH/HOME/locale/TMP/terminal/proxy/TLS/toolchain homes, RLM_*, PRIME_AGENT_*, PYTHON*, UV_*, PIP_*, LC_*, XDG_*, GIT_*, the Windows system set) plus what the session injects (RLM_*, PRIME_AGENT_BASH_*, SERPER_API_KEY when the websearch skill is loaded — now handed over explicitly instead of relying on inheritance). Names that denote credentials (the env-api-keys.ts / prime-agent.sh --no-env list, *_API_KEY, *_TOKEN, *SECRET*, *PASSWORD*, AWS/GCP ambient credential vars) are dropped even under an allowed prefix and listed in the kernel diagnostics. kernel.envPassthrough (settings) admits extra names or PREFIX* globs. bash() children inherit the kernel env, so they see the same set.
  2. Snapshot opt-out and transcript permissions. --no-kernel-snapshots (CLI, also accepted by prime-agent daemon session) or kernel.stateSnapshots: false (settings) disables writing and restoring kernel-state.dill for the session while keeping the kernel stderr log. New transcripts (sessions/<id>.jsonl, forks) are created 0600; existing files keep their mode.
  3. Snapshot redaction. At spawn the host records SHA-256 digests of credential values in its env (and of injected keys) and sends them with each snapshot request as redact_sha256; the runtime skips top-level str/bytes names whose digest (or the stripped form's) matches and reports them in skipped / kernel-state.json with reason matches a credential from the host environment. The kernel never receives the values.

Docs: settings.md (Kernel section), usage.md, sessions.md, rlm-runtime.md, README, repl.md protocol, prime-intellect inference reference. Model-facing surface unchanged (tool names, system prompt, rlm API); the host-only snapshot protocol request gains an optional field.

Behaviour note: kernel code that relied on inheriting PRIME_API_KEY (e.g. direct Prime Inference calls) must read ~/.prime/config.json or opt in via kernel.envPassthrough; the prime CLI is unaffected.

Validation

Local (macOS): npm run check clean. test/kernel-env.test.ts, ipython-provisioner, session-manager/file-operations, session-manager-flush, agent-session-config, settings-manager, kernel-state-snapshot, kernel-bash-shell, kernel-windows-process, args, daemon-command, main-interactive-routing: pass. repl-kernel-*.test.ts incl. new repl-kernel-secret-retention.test.ts (with --tagsFilter kernel-heavy): pass. pytest test/test_repl.py: 106 passed.

Prime sandbox (node:24-bookworm, user tester, synthetic keys only), fixture eng5342-check.ts run on main @ 427ea4c and on this branch with each tree's own runtime venv:

Check main branch
os.environ['PRIME_API_KEY'] in a cell synthetic key None
credential-like names in kernel env OPENAI_API_KEY, PRIME_API_KEY none (env 44 → 12 vars, PATH/HOME/RLM_DEPTH kept)
sh -c 'echo $PRIME_API_KEY' from the kernel synthetic key unset
snapshot of launcher_key = <key> saved; dill contains both keys skipped (matches a credential from the host environment); dill contains neither
resume launcher_key recovered None (other names restored)
sessions/<id>.jsonl / fork mode 0644 / 0644 0600 / 0600
provisioner stateSnapshots: false n/a (dill written) no dill, stderr log present

Branch tests in the sandbox: kernel-heavy files 4/4 pass (repl-kernel-secret-retention, state-roundtrip, mcp-shutdown, parent-watchdog), test_repl.py 106 passed, 16/17 unit files pass. The one failing file, resource-loader.test.ts (4 symlinked-extension tests), fails identically on unpatched main in the same sandbox: pre-existing, unrelated.

Not validated: Windows (env-name case handling covered by unit tests only); the daemon-worker path was exercised through the shared createAgentSessionFromServices wiring, not a live daemon.

…its snapshots

Spawn the REPL kernel with an allowlisted environment instead of the full
host environment, skip snapshotting names that hold a known host credential
value, add --no-kernel-snapshots / kernel.stateSnapshots to disable kernel
state persistence, and create session transcripts owner-only.

Linear: ENG-5342
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Prime Agent performance — completed

PR 5acf6dbd compared with main f9c7e06b.
↓ improved · ↑ regressed · ≈ no clear change · — unavailable

Metric Main This PR Change Change % Result
Cold startup 2,884.5 ms 2,889.4 ms ≈ +4.9 ms +0.17% no clear change
Warm startup 1,677.9 ms 1,698.8 ms ≈ +20.9 ms +1.25% no clear change
Installation 27.16 s 28.60 s ≈ +1.44 s +5.29% no clear change
Compressed release artifacts 11.10 MB 11.11 MB ≈ +0.01 MB +0.13% no clear change
Installed footprint 597.35 MB 597.43 MB ≈ +0.08 MB +0.01% no clear change
Idle memory, summed RSS 1,019.71 MB 1,029.40 MB ≈ +9.69 MB +0.95% no clear change

Python runtime

Metric Main This PR Change Change % Result
Python kernel startup 127.6 ms 128.4 ms ≈ +0.7 ms +0.58% no clear change
Python cell round trip 0.518 ms 0.540 ms ≈ +0.022 ms +4.18% no clear change
Empty bash command 12.0 ms 11.4 ms ≈ -0.6 ms -4.69% no clear change
Bash git status 18.3 ms 17.5 ms ≈ -0.7 ms -4.04% no clear change
Bash 32 KiB output 12.9 ms 11.9 ms ≈ -1.0 ms -8.02% no clear change
35 cells / 9 shell calls 179.4 ms 183.3 ms ≈ +3.9 ms +2.18% no clear change
Python interrupt to done 1.725 ms 1.503 ms ≈ -0.222 ms -12.88% no clear change
Python state snapshot 26.5 ms 26.0 ms ≈ -0.5 ms -1.80% no clear change
Python state restore 370.9 ms 377.4 ms ≈ +6.5 ms +1.74% no clear change
Python idle RSS 35.38 MB 32.95 MB ≈ -2.43 MB -6.87% no clear change
Python RSS after pandas workload 97.82 MB 95.68 MB ≈ -2.14 MB -2.19% no clear change

Sandbox cost: ~$0.0876 — no inference calls.
Run, logs, and downloadable raw results

Methodology and samples

Main resolved at 2026-09-09T23:53:56.704948+00:00. Harness f9c7e06b.
Linux x64, 4 vCPU, 8 GB RAM, 20 GB disk; region us.
Image: node:24-bookworm@sha256:be23f54a88d34e8824c741b19b91064094f92c1c97b194144bfc8b50d67258e2.
Stock tools, skills, daemon, and Python bootstrap enabled; fresh homes and a fixed Git fixture.
Onboarding is dismissed; the editor starts without a selected model or submitted prompt.
Medians shown. Arrows require a 20% timing/memory change plus absolute floors and IQR.
These practical noise floors are not a statistical significance test.
Cold means stopped Prime processes; OS filesystem caches are not flushed.
No model requests or credentials. Installation excludes build/setup time.
Installer tarballs use loopback; npm/Python downloads use the network with fresh caches.
Artifact size counts release tarballs; footprint after first use includes registry packages.
MB is decimal. Summed RSS can double-count shared pages; PSS is recorded when available.
Provisioning, setup, and build durations are recorded separately in the raw results.
Kernel probes use the installed JSONL runtime, outside the TUI/TypeScript host.
Per trial: 50 Python cells, 5 calls per shell case, and one 35-cell mix (9 git status calls).
Cell/shell values are batch means; other runtime timings are single operations.
State fixture: a 10,000-row × 8-column integer DataFrame and a 10,000-integer list.
Restore runs in a fresh kernel, including pandas imports; kernel startup is excluded.
Kernel RSS covers the isolated Python process; loaded RSS follows the pandas workload.
Costs estimate full sandbox lifetimes at configured rates, including setup and build.
Budget target: $1; not a billing cap. Checks are informational.

Metric Main successful/attempted PR successful/attempted Main spread PR spread
Cold startup 10/10 10/10 IQR 123.0 ms IQR 127.5 ms
Warm startup 10/10 10/10 IQR 104.2 ms IQR 110.9 ms
Installation 3/3 3/3 range 1.85 s range 2.49 s
Compressed release artifacts 1/1 1/1
Installed footprint 1/1 1/1
Idle memory, summed RSS 10/10 10/10 IQR 39.38 MB IQR 80.72 MB
Python kernel startup 10/10 10/10 IQR 9.6 ms IQR 5.4 ms
Python cell round trip 10/10 10/10 IQR 0.031 ms IQR 0.065 ms
Empty bash command 10/10 10/10 IQR 1.6 ms IQR 0.5 ms
Bash git status 10/10 10/10 IQR 1.1 ms IQR 1.8 ms
Bash 32 KiB output 10/10 10/10 IQR 1.7 ms IQR 0.6 ms
35 cells / 9 shell calls 10/10 10/10 IQR 19.5 ms IQR 16.7 ms
Python interrupt to done 10/10 10/10 IQR 0.670 ms IQR 0.127 ms
Python state snapshot 10/10 10/10 IQR 3.3 ms IQR 2.3 ms
Python state restore 10/10 10/10 IQR 21.5 ms IQR 43.1 ms
Python idle RSS 10/10 10/10 IQR 3.10 MB IQR 4.55 MB
Python RSS after pandas workload 10/10 10/10 IQR 2.96 MB IQR 6.14 MB

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant